﻿#region 참조 네임스페이스

using System;
using System.IO;
using System.Net;
using System.Text;

#endregion

namespace Skying.Net.Sockets
{
	/// <summary>
	/// <see cref="TcpListener"/>와 <see cref="TcpClient"/>가 서로 통신할 때 주고 받는 헤더로 데이터의 안전한 송신은 물론 데이터 수신 후 처리 과정을 위한 부가 정보도 포함한다.
	/// </summary>
	public class Header
	{
		#region 상수

		/// <summary>
		/// 패킷을 구분하는 구분자 상수를 나타낸다.
		/// </summary>
		public static readonly byte[] _PACKET_SEPERATOR = new byte[] { 255, 0, 255, 0, 255 };

		/// <summary>
		/// 패킷 아이디의 char 길이 상수를 나타낸다.
		/// </summary>
		public const int _ID_CHAR_LENGTH = 10;

		/// <summary>
		/// 일반 헤더 클래스의 기본 크기 상수를 나타낸다.
		/// </summary>
		/// <remarks>
		/// 직렬화 객체를 포함한 일반 데이터 전송의 경우에는 다음과 같은 패킷 구조를 갖는다.
		/// | ID | Packet Type | Encryption Option | Header Size | Body Size | Data Type Size | Data Type |
		/// </remarks>
		public const int _DEFAULT_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + sizeof(PacketType) + sizeof(EncryptionOptions) + sizeof(int) + sizeof(int) + sizeof(int);

		/// <summary>
		/// 파일 헤더 클래스의 기본 크기 상수를 나타낸다.
		/// </summary>
		/// <remarks>
		/// 파일 전송의 경우에는 다음과 같은 패킷 구조를 갖는다.
		/// | ID | Packet Type | Encryption Option | Header Size | File Size | File Name Size | File Name |
		/// </remarks>
		public const int _FILE_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + sizeof(PacketType) + sizeof(EncryptionOptions) + sizeof(int) + sizeof(long) + sizeof(int);

		/// <summary>
		/// 파일 헤더 클래스의 기본 크기 상수를 나타낸다.
		/// </summary>
		/// <remarks>
		/// 파일 전송의 경우에는 다음과 같은 패킷 구조를 갖는다.
		/// | ID | Packet Type | Encryption Option | Header Size | File Size | Chunked Index | Chunked Size | File Name Size | File Name |
		/// </remarks>
		public const int _CHUNKED_HEADER_CLASS_SIZE = _ID_CHAR_LENGTH + sizeof(PacketType) + sizeof(EncryptionOptions) + sizeof(int) + sizeof(long) + sizeof(int) + sizeof(int) + sizeof(int);

		#endregion

		#region 변수

		/// <summary>
		/// 패킷의 아이디를 나타낸다. 아이디는 각 partial 패킷마다 동일하다.
		/// </summary>
		private readonly char[] _id;

		/// <summary>
		/// Header의 크기를 나타낸다.
		/// </summary>
		private readonly int _headerSize;

		/// <summary>
		/// Body의 크기를 나타낸다.
		/// </summary>
		private readonly int _bodySize;

		/// <summary>
		/// 송신하려는 데이터 객체 타입의 크기를 나타낸다.
		/// </summary>
		private readonly int _dataTypeSize;

		/// <summary>
		/// 송신하려는 데이터의 객체 타입을 나타낸다.
		/// </summary>
		private readonly string _dataType;

		/// <summary>
		/// 파일의 크기를 나타낸다.
		/// </summary>
		private readonly long _fileSize;

		/// <summary>
		/// 파일 명칭의 크기를 나타낸다.
		/// </summary>
		private readonly int _fileNameSize;

		/// <summary>
		/// 확장자를 포함하는 파일 명칭을 나타낸다.
		/// </summary>
		private readonly string _fileName;

		/// <summary>
		/// 파일을 부분적으로 나눌 때의 인덱스를 나타낸다.
		/// </summary>
		private readonly int _chunkedIndex;

		/// <summary>
		/// 파일을 부분적으로 나눌 때의 크기를 나타낸다.
		/// </summary>
		private readonly int _chunkedSize;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 패킷의 아이디를 가져온다.
		/// </summary>
		public char[] Id
		{
			get
			{
				return this._id;
			}
		}

		/// <summary>
		/// 패킷의 유형을 가져온다.
		/// </summary>
		public PacketType PacketType
		{
			get;
			private set;
		}

		/// <summary>
		/// 패킷 암호화 옵션을 가져온다.
		/// </summary>
		public EncryptionOptions EncryptionOption
		{
			get;
			private set;
		}

		/// <summary>
		/// 헤더의 크기를 가져온다.
		/// </summary>
		public int HeaderSize
		{
			get
			{
				return this._headerSize;
			}
		}

		/// <summary>
		/// 본문의 크기를 가져온다.
		/// </summary>
		public int BodySize
		{
			get
			{
				return this._bodySize;
			}
		}

		/// <summary>
		/// 본문 데이터 객체 타입의 크기를 가져온다.
		/// </summary>
		public int DataTypeSize
		{
			get
			{
				return this._dataTypeSize;
			}
		}

		/// <summary>
		/// 본문 데이터의 객체 타입을 가져온다.
		/// </summary>
		public string DataType
		{
			get
			{
				return this._dataType;
			}
		}

		/// <summary>
		/// 파일의 크기를 가져온다.
		/// </summary>
		public long FileSize
		{
			get
			{
				return this._fileSize;
			}
		}

		/// <summary>
		/// 파일 명칭의 크기를 가져온다.
		/// </summary>
		public int FileNameSize
		{
			get
			{
				return this._fileNameSize;
			}
		}

		/// <summary>
		/// 확장자를 포함하는 파일 명칭을 가져온다.
		/// </summary>
		public string FileName
		{
			get
			{
				return this._fileName;
			}
		}

		/// <summary>
		/// 파일을 부분적으로 나눌 때의 인덱스를 가져온다.
		/// </summary>
		public int ChunkedIndex
		{
			get
			{
				return this._chunkedIndex;
			}
		}

		/// <summary>
		/// 파일을 부분적으로 나눌 때의 크기를 가져온다.
		/// </summary>
		public int ChunkedSize
		{
			get
			{
				return this._chunkedSize;
			}
		}

		#endregion

		#region 생성자/소멸자

		private Header(char[] id, PacketType packetType, EncryptionOptions encryptionOption = EncryptionOptions.None)
		{
			this._id = id;
			
			this.PacketType = packetType;
			this.EncryptionOption = encryptionOption;
		}

		private Header(char[] id, int bodySize, string dataType, PacketType packetType, EncryptionOptions encryptionOption = EncryptionOptions.None)
			: this(id, packetType, encryptionOption)
		{
			this._bodySize = bodySize;

			this._dataTypeSize = Encoding.UTF8.GetBytes(dataType).Length;
			this._dataType = dataType;

			this._headerSize = _PACKET_SEPERATOR.Length + _DEFAULT_HEADER_CLASS_SIZE + this._dataTypeSize;
		}

		private Header(char[] id, long fileSize, string fileName)
			: this(id, PacketType.File)
		{
			this._fileSize = fileSize;
			this._fileNameSize = Encoding.UTF8.GetBytes(fileName).Length;
			this._fileName = fileName;

			this._headerSize = _PACKET_SEPERATOR.Length + _FILE_HEADER_CLASS_SIZE + this._fileNameSize;
		}

		#endregion

		#region 헤더 생성

		/// <summary>
		/// 패킷 아이디를 생성한다.
		/// </summary>
		/// <remarks>
		/// 패킷 아이디는 생성 시간의 HHmmssfff 포맷에 한자리 숫자의 Mark로 구성된다.
		/// </remarks>
		/// <returns>새로 생성된 패킷 아이디를 반환한다.</returns>
		private static char[] CreatePacketId()
		{
			char[] id = new char[_ID_CHAR_LENGTH];

			try
			{
				DateTime now = DateTime.Now;

				// Mark를 위한 난수를 생성한다.
				Random random = new Random((int)now.Ticks);

				// 0과 9 사이의 난수를 발생시킨다.
				int randomMark = random.Next(0, 9);

				// 생성 시간을 가져온다.
				var nowCharArray = now.ToString("HHmmssfff").ToCharArray();

				Array.Copy(nowCharArray, id, nowCharArray.Length);

				id[9] = (char)randomMark;
			}
			catch (Exception error)
			{
				throw error;
			}

			return id;
		}

		/// <summary>
		/// 내부 헤더를 사용하지 않는 경우에 사용되는 헤더를 생성한다.
		/// </summary>
		/// <remarks>
		/// <para>
		/// <see cref="Skying.Net.Sockets.BaseSocket.UseInternalHeader"/> 값이 false인 경우에만 사용된다.
		/// </para>
		/// </remarks>
		/// <returns>내부 헤더를 사용하지 않는 경우에 사용되는 헤더가 반환된다.</returns>
		internal static Header GenerateHeader()
		{
			return new Header(null, PacketType.Raw);
		}

		/// <summary>
		/// 헤더를 생성한다.
		/// </summary>
		/// <param name="bodySize">원문의 크기를 설정한다.</param>
		/// <param name="dataType"></param>
		/// <param name="packetType">패킷의 유형을 설정한다.</param>
		/// <param name="encryptionOption">암호화 옵션을 설정한다.</param>
		/// <param name="headerBinary">헤더의 바이너리를 참조한다.</param>
		/// <param name="isLittleEndian"></param>
		/// <returns>조건에 의해 생성된 헤더가 반환된다.</returns>
		internal static Header GenerateHeader(int bodySize, string dataType, PacketType packetType, EncryptionOptions encryptionOption, out byte[] headerBinary, bool isLittleEndian = true)
		{
			Header header = new Header(CreatePacketId(), bodySize, dataType, packetType, encryptionOption);

			try
			{
				using (MemoryStream stream = new MemoryStream(header.HeaderSize))
				{
					// 전체 패킷의 시작을 알리는 구분자를 추가한다.
					stream.Write(_PACKET_SEPERATOR, 0, _PACKET_SEPERATOR.Length);

					byte[] idArray = Encoding.UTF8.GetBytes(header.Id);

					// 패킷 아이디에 대한 바이너리를 만들어 스트림에 저장한다.
					stream.Write(idArray, 0, idArray.Length);

					// 패킷 유형을 스트림에 저장한다.
					stream.WriteByte((byte)header.PacketType);

					// 암호화 옵션을 스트림에 저장한다.
					stream.WriteByte((byte)header.EncryptionOption);

					// 헤더 크기에 대한 바이너리를 만들어 스트림에 저장한다.
					var headerSizeBinary = BitConverter.GetBytes(isLittleEndian ? header.HeaderSize : IPAddress.HostToNetworkOrder(header.HeaderSize));

					stream.Write(headerSizeBinary, 0, headerSizeBinary.Length);

					// 본문 크기에 대한 바이너리를 만들어 스트림에 저장한다.
					var bodySizeBinary = BitConverter.GetBytes(isLittleEndian ? header.BodySize : IPAddress.HostToNetworkOrder(header.BodySize));

					stream.Write(bodySizeBinary, 0, bodySizeBinary.Length);

					// 본문 데이터 객체 타입의 크기에 대한 바이너를 만들어 스트림에 저장한다.
					var dataTypeSizeBinary = BitConverter.GetBytes(isLittleEndian ? header.DataTypeSize : IPAddress.HostToNetworkOrder(header.DataTypeSize));

					stream.Write(dataTypeSizeBinary, 0, dataTypeSizeBinary.Length);

					// 본문 데이터 객체 타입에 대한 바이너리를 만들어 스트림에 저장한다.
					var dataTypeBinary = Encoding.UTF8.GetBytes(dataType);

					stream.Write(dataTypeBinary, 0, dataTypeBinary.Length);

					headerBinary = new byte[header.HeaderSize];

					stream.Seek(0, SeekOrigin.Begin);

					stream.Read(headerBinary, 0, header.HeaderSize);

					stream.Flush();
				}
			}
			catch (Exception error)
			{
				throw error;
			}

			return header;
		}

		/// <summary>
		/// 파일 전송을 위한 헤더를 생성한다.
		/// </summary>
		/// <param name="fileSize">파일의 크기를 설정한다.</param>
		/// <param name="fileName">파일의 명칭을 설정한다.</param>
		/// <param name="headerBinary">헤더 바이너리를 참조한다.</param>
		/// <returns>조건에 의해 생성된 헤더가 반환된다.</returns>
		internal static Header GenerateHeader(long fileSize, string fileName, out byte[] headerBinary, bool isLittleEndian = true)
		{
			var header = new Header(CreatePacketId(), fileSize, fileName);

			try
			{
				using (MemoryStream stream = new MemoryStream(header.HeaderSize))
				{
					// 전체 패킷의 시작을 알리는 구분자를 추가한다.
					stream.Write(_PACKET_SEPERATOR, 0, _PACKET_SEPERATOR.Length);

					byte[] idArray = Encoding.UTF8.GetBytes(header.Id);

					// 패킷 아이디에 대한 바이너리를 만들어 스트림에 저장한다.
					stream.Write(idArray, 0, idArray.Length);

					// 패킷 유형을 스트림에 저장한다.
					stream.WriteByte((byte)header.PacketType);

					// 암호화 옵션을 스트림에 저장한다.
					stream.WriteByte((byte)header.EncryptionOption);

					// 헤더 크기에 대한 바이너리를 만들어 스트림에 저장한다.
					var headerSizeBinary = BitConverter.GetBytes(isLittleEndian ? header.HeaderSize : IPAddress.HostToNetworkOrder(header.HeaderSize));

					stream.Write(headerSizeBinary, 0, headerSizeBinary.Length);

					// 파일 크기에 대한 바이너리를 만들어 스트림에 저장한다.
					var fileSizeBinary = BitConverter.GetBytes(isLittleEndian ? header.FileSize : IPAddress.HostToNetworkOrder(header.FileSize));

					stream.Write(fileSizeBinary, 0, fileSizeBinary.Length);

					// 파일 명칭의 크기에 대한 바이너리를 만들어 스트림에 저장한다.
					var fileNameSizeBinary = BitConverter.GetBytes(isLittleEndian ? header.FileNameSize : IPAddress.HostToNetworkOrder(header.FileNameSize));

					stream.Write(fileNameSizeBinary, 0, fileNameSizeBinary.Length);

					// 파일 명칭에 대한 바이너리를 만들어 스트림에 저장한다.
					var fileNameBinary = Encoding.UTF8.GetBytes(header.FileName);

					stream.Write(fileNameBinary, 0, fileNameBinary.Length);

					headerBinary = new byte[header.HeaderSize];

					stream.Seek(0, SeekOrigin.Begin);

					stream.Read(headerBinary, 0, (int)stream.Length);

					stream.Flush();
				}
			}
			catch (Exception error)
			{
				throw error;
			}

			return header;
		}

		#endregion

		#region 헤더 분석

		/// <summary>
		/// 헤더 후보자에서 구분자의 위치를 찾는다.
		/// </summary>
		/// <param name="candidate">헤더 후보자를 설정한다.</param>
		/// <returns>헤더 후보자 내에서의 구분자의 위치를 반환한다. 구분자가 일치하는 구간이 존재하지 않을 경우 -1이 반환된다.</returns>
		public static int SearchSeparatorFor(byte[] candidate)
		{
			int result = -1;

			bool matched = false;

			// 헤더 후보자는 구분자보다 길어야 한다.
			if (candidate.Length > 0 &&
				_PACKET_SEPERATOR.Length > 0 &&
				candidate.Length >= _PACKET_SEPERATOR.Length)
			{
				// 후보자를 검색한다.
				for (int index1 = 0; index1 <= candidate.Length - _PACKET_SEPERATOR.Length; index1++)
				{
					// 구분자의 첫번째 아이템과 일치하는 구간(구분자와 일치하는 시작점)을 찾는다.
					if (candidate[index1] == _PACKET_SEPERATOR[0])
					{
						// 후보자의 크기가 1보다 크면 구분자 전체와 비교하여 일치하는지 검사한다.
						if (candidate.Length > 1)
						{
							matched = true;

							// 구분자와 일치하는 시작점으로부터 구분자 전체와 비교한다.
							for (int index2 = 1; index2 <= _PACKET_SEPERATOR.Length - 1; index2++)
							{
								if (candidate[index1 + index2] != _PACKET_SEPERATOR[index2])
								{
									matched = false;

									break;
								}
							}

							if (matched)
							{
								result = index1;

								break;
							}
						}
						// 후보자의 크기가 1보다 크지 않으면 종료 처리한다.
						else
						{
							result = index1;

							break;
						}
					}
				}
			}

			return result;
		}

		/// <summary>
		/// 헤더 후보자에서 헤더를 분석한다.
		/// </summary>
		/// <param name="headerCandidate">헤더 후보자를 설정한다.</param>
		/// <param name="result">헤더 분석 결과를 참조한다.</param>
		/// <returns>분석된 헤더를 반환한다.</returns>
		internal static Header ParseHeader(byte[] headerCandidate, out HeaderParseResult result, bool isLittleEndian = true)
		{
			Header header = null;

			try
			{
				// 헤더 후보자가 null이거나 크기가 0인 경우, 오류로 분류한다.
				if (headerCandidate == null || headerCandidate.Length == 0)
				{
					result = HeaderParseResult.InvalidHeaderCandidate;

					return null;
				}

				int offset = 0;

				byte[] separator = new byte[_PACKET_SEPERATOR.Length];

				Array.Copy(headerCandidate, separator, separator.Length);

				// 패킷 시작점이 존재하지 않는 경우, 오류로 분류한다.
				if (Header.SearchSeparatorFor(headerCandidate) == -1)
				{
					result = HeaderParseResult.NoSeparator;

					return null;
				}

				offset += _PACKET_SEPERATOR.Length;

				byte[] idArray = new byte[_ID_CHAR_LENGTH];

				if (idArray.Length > headerCandidate.Length)
				{
					result = HeaderParseResult.InvalidHeaderCandidate;

					return null;
				}

				Array.Copy(headerCandidate, offset, idArray, 0, _ID_CHAR_LENGTH);

				// 패킷 아이디를 읽어온다.
				char[] id = Encoding.UTF8.GetChars(idArray);

				offset += _ID_CHAR_LENGTH;

				// 패킷 유형을 읽어온다.
				PacketType packetType = (PacketType)headerCandidate[offset];

				if (!Enum.IsDefined(typeof(PacketType), packetType))
				{
					result = HeaderParseResult.InvalidPacketType;

					return null;
				}

				offset++;

				// 암호화 옵션을 읽어온다.
				EncryptionOptions encryptionOption = (EncryptionOptions)headerCandidate[offset];

				if (!Enum.IsDefined(typeof(EncryptionOptions), encryptionOption))
				{
					result = HeaderParseResult.InvalidEncryptionOptions;

					return null;
				}

				offset++;

				// 헤더 크기를 읽어온다.
				int headerSize = BitConverter.ToInt32(headerCandidate, offset);

				if (!isLittleEndian)
				{
					headerSize = IPAddress.HostToNetworkOrder(headerSize);
				}

				offset += sizeof(int);

				switch (packetType)
				{
					case PacketType.Raw:
					case PacketType.Serialized:
					case PacketType.Message:

						// 분문 크기를 읽어온다.
						int bodySize = BitConverter.ToInt32(headerCandidate, offset);

						if (!isLittleEndian)
						{
							bodySize = IPAddress.HostToNetworkOrder(bodySize);
						}

						offset += sizeof(int);

						// 본문 데이터 타입의 크기를 읽어온다.
						int dataTypeSize = BitConverter.ToInt32(headerCandidate, offset);

						if (!isLittleEndian)
						{
							dataTypeSize = IPAddress.HostToNetworkOrder(dataTypeSize);
						}

						offset += sizeof(int);

						// 헤더 크기가 올바르지 않은 경우, 오류로 분류한다.
						if (headerSize != _PACKET_SEPERATOR.Length + _DEFAULT_HEADER_CLASS_SIZE + dataTypeSize)
						{
							result = HeaderParseResult.HeaderSizeNotMatch;

							return null;
						}

						// 본문 데이터 타입을 읽어온다.
						string dataType = Encoding.UTF8.GetString(headerCandidate, offset, dataTypeSize);

						offset += dataTypeSize;

						header = new Header(id, bodySize, dataType, packetType, encryptionOption);

						break;

					case PacketType.File:

						// 파일 크기를 읽어온다.
						long fileSize = BitConverter.ToInt64(headerCandidate, offset);

						if (!isLittleEndian)
						{
							fileSize = IPAddress.HostToNetworkOrder(fileSize);
						}

						offset += sizeof(long);

						int fileNameSize = BitConverter.ToInt32(headerCandidate, offset);

						if (!isLittleEndian)
						{
							fileNameSize = IPAddress.HostToNetworkOrder(fileNameSize);
						}

						offset += sizeof(int);

						// 헤더 크기가 올바르지 않은 경우, 오류로 분류한다.
						if (headerSize != _PACKET_SEPERATOR.Length + _FILE_HEADER_CLASS_SIZE + fileNameSize)
						{
							result = HeaderParseResult.HeaderSizeNotMatch;

							return null;
						}

						string fileName = Encoding.UTF8.GetString(headerCandidate, offset, fileNameSize);

						offset += fileNameSize;

						header = new Header(id, fileSize, fileName);

						break;
				}

				result = HeaderParseResult.Success;
			}
			catch (Exception error)
			{
				throw error;
			}

			return header;
		}

		#endregion
	}
}